import json
from math import log
from numbers import Number

from logbook import Logger

import eos.db
from config import getVersion
from service.fit import Fit
from service.market import Market
from eos.const import FittingModuleState, FittingHardpoint, FittingSlot
from service.const import PortEftRigSize
from eos.saveddata.module import Module
from eos.saveddata.drone import Drone
from eos.effectHandlerHelpers import HandledList
from eos.db import gamedata_session, getCategory, getAttributeInfo, getGroup
from eos.gamedata import Attribute, Effect, Group, Item, ItemEffect
from eos.utils.spoolSupport import SpoolType, SpoolOptions
from gui.fitCommands.calc.module.localAdd import CalcAddLocalModuleCommand
from gui.fitCommands.calc.module.localRemove import CalcRemoveLocalModulesCommand
from gui.fitCommands.calc.module.changeCharges import CalcChangeModuleChargesCommand
from gui.fitCommands.helpers import ModuleInfo


pyfalog = Logger(__name__)
pyfaVersion = getVersion()


class EfsPort:
    wepTestSet = {}
    version = 0.04

    @staticmethod
    def attrDirectMap(values, target, source):
        for val in values:
            target[val] = source.getModifiedItemAttr(val)

    @staticmethod
    def getT2MwdSpeed(fit, sFit):
        fitID = fit.ID
        propID = None
        shipHasMedSlots = fit.ship.getModifiedItemAttr("medSlots") > 0
        shipPower = fit.ship.getModifiedItemAttr("powerOutput")
        # Monitors have a 99% reduction to prop mod power requirements
        if fit.ship.name == "监视者级":
            shipPower *= 100
        rigSize = fit.ship.getModifiedItemAttr("rigSize")
        if not shipHasMedSlots:
            return None

        filterVal = Item.groupID == getGroup("推进装备").ID
        propMods = gamedata_session.query(Item).options().filter(filterVal).all()
        mapPropData = lambda propName: \
                      next(map(lambda propMod: {"id": propMod.typeID, "powerReq": propMod.attributes["power"].value},
                               (filter(lambda mod: mod.name == propName, propMods))))
        mwd5mn = mapPropData("5MN微型跃迁推进器 II")
        mwd50mn = mapPropData("50MN微型跃迁推进器 II")
        mwd500mn = mapPropData("500MN微型跃迁推进器 II")
        mwd50000mn = mapPropData("50000MN微型跃迁推进器 II")
        if rigSize == PortEftRigSize.SMALL or rigSize is None:
            propID = mwd5mn["id"] if shipPower > mwd5mn["powerReq"] else None
        elif rigSize == PortEftRigSize.MEDIUM:
            propID = mwd50mn["id"] if shipPower > mwd50mn["powerReq"] else mwd5mn["id"]
        elif rigSize == PortEftRigSize.LARGE:
            propID = mwd500mn["id"] if shipPower > mwd500mn["powerReq"] else mwd50mn["id"]
        elif rigSize == PortEftRigSize.CAPITAL:
            propID = mwd50000mn["id"] if shipPower > mwd50000mn["powerReq"] else mwd500mn["id"]

        if propID is None:
            return None
        cmd = CalcAddLocalModuleCommand(fitID, ModuleInfo(itemID=propID))
        cmd.Do()
        if cmd.needsGuiRecalc:
            sFit.recalc(fit)
        fit = eos.db.getFit(fitID)
        mwdPropSpeed = fit.maxSpeed
        mwdPosition = list(filter(lambda mod: mod.item and mod.item.ID == propID, fit.modules))[0].position
        cmd = CalcRemoveLocalModulesCommand(fitID, [mwdPosition])
        cmd.Do()
        if cmd.needsGuiRecalc:
            sFit.recalc(fit)
        fit = eos.db.getFit(fitID)
        return mwdPropSpeed

    @staticmethod
    def getPropData(fit, sFit):
        propMods = filter(lambda mod: mod.item and mod.item.group.name == "推进装备", fit.modules)
        activePropWBloomFilter = lambda mod: mod.state > 0 and "signatureRadiusBonus" in mod.item.attributes
        propWithBloom = next(filter(activePropWBloomFilter, propMods), None)
        if propWithBloom is not None:
            oldPropState = propWithBloom.state
            propWithBloom.state = FittingModuleState.ONLINE
            sFit.recalc(fit)
            sp = fit.maxSpeed
            sig = fit.ship.getModifiedItemAttr("signatureRadius")
            propWithBloom.state = oldPropState
            sFit.recalc(fit)
            return {"usingMWD": True, "unpropedSpeed": sp, "unpropedSig": sig}
        return {
            "usingMWD": False,
            "unpropedSpeed": fit.maxSpeed,
            "unpropedSig": fit.ship.getModifiedItemAttr("signatureRadius")
        }

    @staticmethod
    def getModsInGroups(fit, modGroupNames):
        matchingMods = list(filter(lambda mod: mod.item and mod.item.group.name in modGroupNames, fit.modules))
        # Sort mods to prevent the order needlessly changing as pyfa updates.
        matchingMods.sort(key=lambda mod: mod.item.ID)
        matchingMods.sort(key=lambda mod: mod.item.group.ID)
        return matchingMods

    # Note this also includes data for any cap boosters as they "repair" cap.
    @staticmethod
    def getRepairData(fit, sFit):
        modGroupNames = [
            "护盾回充增量器", "装甲维修组件",
            "辅助护盾回充增量器", "辅助装甲维修器",
            "船体维修组件", "电容注电器",
        ]
        repairMods = EfsPort.getModsInGroups(fit, modGroupNames)
        repairs = [];
        for mod in repairMods:
            stats = {}
            EfsPort.attrDirectMap(["duration", "capacitorNeed"], stats, mod)
            if mod.item.group.name in ["装甲维修组件", "辅助装甲维修器"]:
                stats["type"] = "Armor Repairer"
                EfsPort.attrDirectMap(["armorDamageAmount"], stats, mod)
                if mod.item.group.name == "辅助装甲维修器":
                    stats["numShots"] = mod.numShots
                    EfsPort.attrDirectMap(["reloadTime", "chargedArmorDamageMultiplier"], stats, mod)
            elif mod.item.group.name in ["护盾回充增量器", "辅助护盾回充增量器"]:
                stats["type"] = "护盾回充增量器"
                EfsPort.attrDirectMap(["shieldBonus"], stats, mod)
                if mod.item.group.name == "辅助护盾回充增量器":
                    stats["numShots"] = mod.numShots
                    EfsPort.attrDirectMap(["reloadTime"], stats, mod)
                    c = mod.charge
                    if c:
                        sFit.recalc(fit)
                        CalcChangeModuleChargesCommand(
                            fit.ID,
                            projected=False,
                            chargeMap={mod.position: None},
                            recalc=False).Do()
                        sFit.recalc(fit)
                        stats["unloadedCapacitorNeed"] = mod.getModifiedItemAttr("capacitorNeed")
                        CalcChangeModuleChargesCommand(
                            fit.ID,
                            projected=False,
                            chargeMap={mod.position: c.typeID},
                            recalc=False).Do()
                        sFit.recalc(fit)
            elif mod.item.group.name == "电容注电器":
                # The capacitorNeed is negative, which provides the boost.
                stats["type"] = "电容注电器"
                stats["numShots"] = mod.numShots
                EfsPort.attrDirectMap(["reloadTime"], stats, mod)
            repairs.append(stats)
        return repairs

    @staticmethod
    def getOutgoingProjectionData(fit):
        # This is a subset of module groups capable of projection and a superset of those currently used by efs
        modGroupNames = [
            "远程护盾回充增量器", "跃迁扰乱设备", "能量滞停光束网", "远程电容传输装置",
            "掠能器", "能量中和器", "脉冲干扰器", "ECM", "感应抑阻器",
            "武器扰断器", "远程装甲维修器", "目标标记装置", "远程船体维修装置",
            "脉冲波投射器", "跃迁扰断力场发生器", "装甲抗性变换增强器",
            "目标锁定扰断装置", "微型跳跃引擎", "舰船模式切换器", "停滞捕捉光束",
            "辅助远程护盾回充增量器", "辅助远程装甲维修器",
            "泰坦现象发生器", "非循环增强器", "突变适应远程装甲维修器"
        ]
        projectedMods = EfsPort.getModsInGroups(fit, modGroupNames)
        projections = []
        for mod in projectedMods:
            maxRangeDefault = 0
            falloffDefault = 0
            stats = {}
            if mod.item.group.name in ["能量滞停光束网", "停滞捕捉光束"]:
                stats["type"] = "能量滞停光束网"
                stats["optimal"] = mod.getModifiedItemAttr("maxRange")
                EfsPort.attrDirectMap(["duration", "speedFactor"], stats, mod)
            elif mod.item.group.name == "武器扰断器":
                stats["type"] = "武器扰断器"
                stats["optimal"] = mod.getModifiedItemAttr("maxRange")
                stats["falloff"] = mod.getModifiedItemAttr("falloffEffectiveness")
                EfsPort.attrDirectMap([
                    "trackingSpeedBonus", "maxRangeBonus", "falloffBonus", "aoeCloudSizeBonus",
                    "aoeVelocityBonus", "missileVelocityBonus", "explosionDelayBonus"
                ], stats, mod)
            elif mod.item.group.name == "掠能器":
                stats["type"] = "掠能器"
                EfsPort.attrDirectMap(["powerTransferAmount", "energyNeutralizerSignatureResolution"], stats, mod)
            elif mod.item.group.name == "能量中和器":
                stats["type"] = "能量中和器"
                EfsPort.attrDirectMap([
                    "energyNeutralizerSignatureResolution", "entityCapacitorLevelModifierSmall",
                    "entityCapacitorLevelModifierMedium", "entityCapacitorLevelModifierLarge",
                    "energyNeutralizerAmount"
                ], stats, mod)
            elif mod.item.group.name in ["远程护盾回充增量器", "辅助远程护盾回充增量器"]:
                stats["type"] = "远程护盾回充增量器"
                EfsPort.attrDirectMap(["shieldBonus"], stats, mod)
            elif mod.item.group.name in [
                    "远程装甲维修器", "辅助远程装甲维修器", "突变适应远程装甲维修器"
            ]:
                stats["type"] = "远程装甲维修器"
                EfsPort.attrDirectMap(["armorDamageAmount"], stats, mod)
            elif mod.item.group.name == "跃迁扰乱设备":
                stats["type"] = "跃迁扰乱设备"
                EfsPort.attrDirectMap(["activationBlockedStrenght", "warpScrambleStrength"], stats, mod)
            elif mod.item.group.name == "跃迁扰断力场发生器":
                maxRangeDefault = mod.getModifiedItemAttr("warpScrambleRange")
                stats["type"] = "跃迁扰乱设备"
                EfsPort.attrDirectMap(["activationBlockedStrenght", "warpScrambleStrength"], stats, mod)
                if maxRangeDefault >= 30000:
                    # We want this to be 0 for disruption scripts as we have no other way to tell scrams from points.
                    stats["activationBlockedStrenght"] = 0
            elif mod.item.group.name == "目标标记装置":
                stats["type"] = "目标标记装置"
                EfsPort.attrDirectMap(["signatureRadiusBonus"], stats, mod)
            elif mod.item.group.name == "感应抑阻器":
                stats["type"] = "感应抑阻器"
                EfsPort.attrDirectMap(["maxTargetRangeBonus", "scanResolutionBonus"], stats, mod)
            elif mod.item.group.name == "ECM":
                stats["type"] = "ECM"
                EfsPort.attrDirectMap([
                    "scanGravimetricStrengthBonus", "scanMagnetometricStrengthBonus",
                    "scanRadarStrengthBonus", "scanLadarStrengthBonus",
                ], stats, mod)
            elif mod.item.group.name == "脉冲干扰器":
                stats["type"] = "脉冲干扰器"
                maxRangeDefault = mod.getModifiedItemAttr("ecmBurstRange")
                EfsPort.attrDirectMap([
                    "scanGravimetricStrengthBonus", "scanMagnetometricStrengthBonus",
                    "scanRadarStrengthBonus", "scanLadarStrengthBonus",
                ], stats, mod)
            elif mod.item.group.name == "微型跳跃引擎":
                stats["type"] = "微型跳跃引擎"
                EfsPort.attrDirectMap(["moduleReactivationDelay"], stats, mod)
            else:
                pyfalog.error("Projected module {0} lacks efs export implementation".format(mod.item.name))
            if mod.getModifiedItemAttr("maxRange", None) is None:
                pyfalog.error("Projected module {0} has no maxRange".format(mod.item.name))
            stats["optimal"] = mod.getModifiedItemAttr("maxRange", maxRangeDefault)
            stats["falloff"] = mod.getModifiedItemAttr("falloffEffectiveness", falloffDefault)
            EfsPort.attrDirectMap(["duration", "capacitorNeed"], stats, mod)
            projections.append(stats)
        return projections

    # Note that unless padTypeIDs is True all 0s will be removed from modTypeIDs in the return.
    # They always are added initially for the sake of brevity, as this option may not be retained long term.
    @staticmethod
    def getModuleInfo(fit, padTypeIDs=False):
        moduleNames = []
        modTypeIDs = []
        moduleNameSets = {FittingSlot.LOW: [], FittingSlot.MED: [], FittingSlot.HIGH: [], FittingSlot.RIG: [], FittingSlot.SUBSYSTEM: []}
        modTypeIDSets = {FittingSlot.LOW: [], FittingSlot.MED: [], FittingSlot.HIGH: [], FittingSlot.RIG: [], FittingSlot.SUBSYSTEM: []}
        for mod in fit.modules:
            try:
                if mod.item is not None:
                    if mod.charge is not None:
                        modTypeIDSets[mod.slot].append([mod.item.typeID, mod.charge.typeID])
                        moduleNameSets[mod.slot].append(mod.item.name + ":  " + mod.charge.name)
                    else:
                        modTypeIDSets[mod.slot].append(mod.item.typeID)
                        moduleNameSets[mod.slot].append(mod.item.name)
                else:
                    modTypeIDSets[mod.slot].append(0)
                    moduleNameSets[mod.slot].append("Empty Slot")
            except (KeyboardInterrupt, SystemExit):
                raise
            except:
                pyfalog.error("Could not find name for module {0}".format(vars(mod)))

        for modInfo in [
            ["High Slots:"], moduleNameSets[FittingSlot.HIGH], ["", "Med Slots:"], moduleNameSets[FittingSlot.MED],
            ["", "Low Slots:"], moduleNameSets[FittingSlot.LOW], ["", "Rig Slots:"], moduleNameSets[FittingSlot.RIG]
        ]:
            moduleNames.extend(modInfo)
        if len(moduleNameSets[FittingSlot.SUBSYSTEM]) > 0:
            moduleNames.extend(["", "Subsystems:"])
            moduleNames.extend(moduleNameSets[FittingSlot.SUBSYSTEM])

        for slotType in [FittingSlot.HIGH, FittingSlot.MED, FittingSlot.LOW, FittingSlot.RIG, FittingSlot.SUBSYSTEM]:
            if slotType is not FittingSlot.SUBSYSTEM or len(modTypeIDSets[slotType]) > 0:
                modTypeIDs.extend([0, 0] if slotType is not FittingSlot.HIGH else [0])
                modTypeIDs.extend(modTypeIDSets[slotType])

        droneNames = []
        droneIDs = []
        fighterNames = []
        fighterIDs = []
        for drone in fit.drones:
            if drone.amountActive > 0:
                droneIDs.append(drone.item.typeID)
                droneNames.append("%s x%s" % (drone.item.name, drone.amount))
        for fighter in fit.fighters:
            if fighter.amount > 0:
                fighterIDs.append(fighter.item.typeID)
                fighterNames.append("%s x%s" % (fighter.item.name, fighter.amount))
        if len(droneNames) > 0:
            modTypeIDs.extend([0, 0])
            modTypeIDs.extend(droneIDs)
            moduleNames.extend(["", "Drones:"])
            moduleNames.extend(droneNames)
        if len(fighterNames) > 0:
            modTypeIDs.extend([0, 0])
            modTypeIDs.extend(fighterIDs)
            moduleNames.extend(["", "Fighters:"])
            moduleNames.extend(fighterNames)
        if len(fit.implants) > 0:
            modTypeIDs.extend([0, 0])
            moduleNames.extend(["", "Implants:"])
            for implant in fit.implants:
                modTypeIDs.append(implant.item.typeID)
                moduleNames.append(implant.item.name)
        if len(fit.boosters) > 0:
            modTypeIDs.extend([0, 0])
            moduleNames.extend(["", "Boosters:"])
            for booster in fit.boosters:
                modTypeIDs.append(booster.item.typeID)
                moduleNames.append(booster.item.name)
        if len(fit.commandFits) > 0:
            modTypeIDs.extend([0, 0])
            moduleNames.extend(["", "Command Fits:"])
            for commandFit in fit.commandFits:
                modTypeIDs.append(commandFit.ship.item.typeID)
                moduleNames.append(commandFit.name)
        if len(fit.projectedModules) > 0:
            modTypeIDs.extend([0, 0])
            moduleNames.extend(["", "Projected Modules:"])
            for mod in fit.projectedModules:
                modTypeIDs.append(mod.item.typeID)
                moduleNames.append(mod.item.name)

        if fit.character.name != "All 5":
            modTypeIDs.extend([0, 0, 0])
            moduleNames.extend(["", "Character:"])
            moduleNames.append(fit.character.name)
        if padTypeIDs is not True:
            modTypeIDsUnpadded = [mod for mod in modTypeIDs if mod != 0]
            modTypeIDs = modTypeIDsUnpadded
        return {"moduleNames": moduleNames, "modTypeIDs": modTypeIDs}

    @staticmethod
    def getFighterAbilityData(fighterAttr, fighter, baseRef):
        baseRefDam = baseRef + "Damage"
        abilityName = "RegularAttack" if baseRef == "fighterAbilityAttackMissile" else "MissileAttack"
        rangeSuffix = "RangeOptimal" if baseRef == "fighterAbilityAttackMissile" else "范围"
        reductionRef = baseRef if baseRef == "fighterAbilityAttackMissile" else baseRefDam
        damageReductionFactor = log(fighterAttr(reductionRef + "ReductionFactor")) / log(fighterAttr(reductionRef + "ReductionSensitivity"))
        damTypes = ["EM", "Therm", "Exp", "Kin"]
        abBaseDamage = sum(map(lambda damType: fighterAttr(baseRefDam + damType), damTypes))
        abDamage = abBaseDamage * fighterAttr(baseRefDam + "Multiplier")
        return {
            "name": abilityName, "volley": abDamage * fighter.amount, "explosionRadius": fighterAttr(baseRef + "ExplosionRadius"),
            "explosionVelocity": fighterAttr(baseRef + "ExplosionVelocity"), "optimal": fighterAttr(baseRef + rangeSuffix),
            "damageReductionFactor": damageReductionFactor, "rof": fighterAttr(baseRef + "单次运转时间"),
        }

    @staticmethod
    def getWeaponSystemData(fit):
        weaponSystems = []
        groups = {}
        # Export at maximum spool for consistency, spoolup data is exported anyway.
        defaultSpoolValue = 1
        spoolOptions = SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, True)
        for mod in fit.modules:
            if mod.getDps(spoolOptions=spoolOptions).total > 0:
                # Group weapon + ammo combinations that occur more than once
                keystr = str(mod.itemID) + "-" + str(mod.chargeID)
                if keystr in groups:
                    groups[keystr][1] += 1
                else:
                    groups[keystr] = [mod, 1]
        for wepGroup in groups.values():
            stats = wepGroup[0]
            n = wepGroup[1]
            tracking = 0
            maxVelocity = 0
            explosionDelay = 0
            damageReductionFactor = 0
            explosionRadius = 0
            explosionVelocity = 0
            aoeFieldRange = 0
            typeing = 'None'
            if stats.charge:
                name = stats.item.name + ", " + stats.charge.name
            else:
                name = stats.item.name
            if stats.hardpoint == FittingHardpoint.TURRET:
                tracking = stats.getModifiedItemAttr("trackingSpeed")
                typeing = "Turret"
            # Bombs share most attributes with missiles despite not needing the hardpoint
            elif stats.hardpoint == FittingHardpoint.MISSILE or "Bomb Launcher" in stats.item.name:
                maxVelocity = stats.getModifiedChargeAttr("maxVelocity")
                explosionDelay = stats.getModifiedChargeAttr("explosionDelay")
                damageReductionFactor = stats.getModifiedChargeAttr("aoeDamageReductionFactor")
                explosionRadius = stats.getModifiedChargeAttr("aoeCloudSize")
                explosionVelocity = stats.getModifiedChargeAttr("aoeVelocity")
                typeing = "Missile"
            # AoE DDs can be treated like missiles with a damageReductionFactor of 0
            elif stats.item.group.name == '超级武器' and stats.maxRange:
                explosionRadius = stats.getModifiedItemAttr("signatureRadius")
                typeing = "Missile"
            elif stats.hardpoint == FittingHardpoint.NONE:
                aoeFieldRange = stats.getModifiedItemAttr("empFieldRange")
                # This also covers non-bomb weapons with dps values and no hardpoints, most notably targeted doomsdays.
                typeing = "SmartBomb"
            # Targeted DDs are the only non drone/fighter weapon without an explicit max range
            if stats.item.group.name == '超级武器' and stats.maxRange is None:
                maxRange = 300000
            else:
                maxRange = stats.maxRange
            statDict = {
                "dps": stats.getDps(spoolOptions=spoolOptions).total * n, "capUse": stats.capUse * n, "falloff": stats.falloff,
                "type": typeing, "name": name, "optimal": maxRange,
                "numCharges": stats.numCharges, "numShots": stats.numShots, "reloadTime": stats.reloadTime,
                "cycleTime": stats.getCycleParameters().averageTime, "volley": stats.getVolley(spoolOptions=spoolOptions).total * n, "tracking": tracking,
                "maxVelocity": maxVelocity, "explosionDelay": explosionDelay, "damageReductionFactor": damageReductionFactor,
                "explosionRadius": explosionRadius, "explosionVelocity": explosionVelocity, "aoeFieldRange": aoeFieldRange,
                "damageMultiplierBonusMax": stats.getModifiedItemAttr("damageMultiplierBonusMax"),
                "damageMultiplierBonusPerCycle": stats.getModifiedItemAttr("damageMultiplierBonusPerCycle")
            }
            weaponSystems.append(statDict)
        for drone in fit.drones:
            if drone.getDps().total > 0 and drone.amountActive > 0:
                droneAttr = drone.getModifiedItemAttr
                # Drones are using the old tracking formula for trackingSpeed. This updates it to match turrets.
                newTracking = droneAttr("trackingSpeed") / (droneAttr("optimalSigRadius") / 40000)
                statDict = {
                    "dps": drone.getDps().total, "cycleTime": drone.getCycleParameters().averageTime, "type": "无人机",
                    "optimal": drone.maxRange, "name": drone.item.name, "falloff": drone.falloff,
                    "maxSpeed": droneAttr("maxVelocity"), "tracking": newTracking,
                    "volley": drone.getVolley().total
                }
                weaponSystems.append(statDict)
        for fighter in fit.fighters:
            if fighter.getDps().total > 0 and fighter.amount > 0:
                fighterAttr = fighter.getModifiedItemAttr
                abilities = []
                if "fighterAbilityAttackMissileDamageEM" in fighter.item.attributes.keys():
                    baseRef = "fighterAbilityAttackMissile"
                    ability = EfsPort.getFighterAbilityData(fighterAttr, fighter, baseRef)
                    abilities.append(ability)
                if "fighterAbilityMissilesDamageEM" in fighter.item.attributes.keys():
                    baseRef = "fighterAbilityMissiles"
                    ability = EfsPort.getFighterAbilityData(fighterAttr, fighter, baseRef)
                    abilities.append(ability)
                statDict = {
                    "dps": fighter.getDps().total, "type": "铁骑舰载机", "name": fighter.item.name,
                    "maxSpeed": fighterAttr("maxVelocity"), "abilities": abilities,
                    "ehp": fighterAttr("shieldCapacity") / 0.8875 * fighter.amount,
                    "volley": fighter.getVolley().total, "signatureRadius": fighterAttr("signatureRadius")
                }
                weaponSystems.append(statDict)
        return weaponSystems

    @staticmethod
    def getTestSet(setType):
        def getT2ItemsWhere(additionalFilter, mustBeOffensive=False, category="装备"):
            # Used to obtain a smaller subset of items while still containing examples of each group.
            T2_META_LEVEL = 5
            categoryID = getCategory(category).categoryID
            result = gamedata_session.query(Item).join(ItemEffect, Group, Attribute).\
                      filter(
                          additionalFilter,
                          Item.metaLevel == T2_META_LEVEL,
                          Group.categoryID == categoryID,
                      ).all()
            if mustBeOffensive:
                result = filter(lambda t: t.offensive is True, result)
            return list(result)

        def getChargeType(item, setType):
            if setType == "turret":
                return str(item.attributes["chargeGroup1"].value) + "-" + str(item.attributes["chargeSize"].value)
            return str(item.attributes["chargeGroup1"].value)

        if setType in EfsPort.wepTestSet.keys():
            return EfsPort.wepTestSet[setType]
        else:
            EfsPort.wepTestSet[setType] = []
        modSet = EfsPort.wepTestSet[setType]

        if setType == "drone":
            ilist = getT2ItemsWhere(True, True, "无人机")
            for item in ilist:
                drone = Drone(item)
                drone.amount = 1
                drone.amountActive = 1
                drone.itemModifiedAttributes.parent = drone
                modSet.append(drone)
            return modSet

        turretFittedEffectID = gamedata_session.query(Effect).filter(Effect.name == "turretFitted").first().effectID
        launcherFittedEffectID = gamedata_session.query(Effect).filter(Effect.name == "launcherFitted").first().effectID
        if setType == "launcher":
            effectFilter = ItemEffect.effectID == launcherFittedEffectID
            reqOff = False
        else:
            effectFilter = ItemEffect.effectID == turretFittedEffectID
            reqOff = True
        ilist = getT2ItemsWhere(effectFilter, reqOff)
        previousChargeTypes = []
        # Get modules from item list
        for item in ilist:
            chargeType = getChargeType(item, setType)
            # Only add turrets if we don"t already have one with the same size and ammo type.
            if setType == "launcher" or chargeType not in previousChargeTypes:
                previousChargeTypes.append(chargeType)
                mod = Module(item)
                modSet.append(mod)

        sMkt = Market.getInstance()
        # Due to typed missile damage bonuses we"ll need to add extra launchers to cover all four types.
        additionalLaunchers = []
        for mod in modSet:
            clist = list(gamedata_session.query(Item).options().
                    filter(Item.groupID == mod.getModifiedItemAttr("chargeGroup1")).all())
            mods = [mod]
            charges = [clist[0]]
            if setType == "launcher":
                # We don"t want variations of missiles we already have
                prevCharges = list(sMkt.getVariationsByItems(charges))
                testCharges = []
                for charge in clist:
                    if charge not in prevCharges:
                        testCharges.append(charge)
                        prevCharges += sMkt.getVariationsByItems([charge])
                for c in testCharges:
                    charges.append(c)
                    additionalLauncher = Module(mod.item)
                    mods.append(additionalLauncher)
            for i in range(len(mods)):
                mods[i].charge = charges[i]
                mods[i].reloadForce = True
                mods[i].state = 2
                if setType == "launcher" and i > 0:
                    additionalLaunchers.append(mods[i])
        modSet += additionalLaunchers
        return modSet

    @staticmethod
    def getWeaponBonusMultipliers(fit):
        def sumDamage(attr):
            totalDamage = 0
            for damageType in ["emDamage", "thermalDamage", "kineticDamage", "explosiveDamage"]:
                if attr(damageType) is not None:
                    totalDamage += attr(damageType)
            return totalDamage

        def getCurrentMultipliers(tf):
            fitMultipliers = {}
            getDroneMulti = lambda d: sumDamage(d.getModifiedItemAttr) * d.getModifiedItemAttr("damageMultiplier")
            fitMultipliers["drones"] = list(map(getDroneMulti, tf.drones))

            getFitTurrets = lambda f: filter(lambda mod: mod.hardpoint == FittingHardpoint.TURRET, f.modules)
            getTurretMulti = lambda mod: mod.getModifiedItemAttr("damageMultiplier") / mod.getCycleParameters().averageTime
            fitMultipliers["turrets"] = list(map(getTurretMulti, getFitTurrets(tf)))

            getFitLaunchers = lambda f: filter(lambda mod: mod.hardpoint == FittingHardpoint.MISSILE, f.modules)
            getLauncherMulti = lambda mod: sumDamage(mod.getModifiedChargeAttr) / mod.getCycleParameters().averageTime
            fitMultipliers["launchers"] = list(map(getLauncherMulti, getFitLaunchers(tf)))
            return fitMultipliers

        multipliers = {"turret": 1, "launcher": 1, "droneBandwidth": 1}
        drones = EfsPort.getTestSet("drone")
        launchers = EfsPort.getTestSet("launcher")
        turrets = EfsPort.getTestSet("turret")
        for weaponTypeSet in [turrets, launchers, drones]:
            for mod in weaponTypeSet:
                mod.owner = fit
        turrets = list(filter(lambda mod: mod.getModifiedItemAttr("damageMultiplier"), turrets))
        launchers = list(filter(lambda mod: sumDamage(mod.getModifiedChargeAttr), launchers))

        # Since the effect modules are fairly opaque a mock test fit is used to test the impact of traits.
        # standin class used to prevent . notation causing issues when used as an arg
        class standin:
            pass
        tf = standin()
        tf.modules = HandledList(turrets + launchers)
        tf.character = fit.character
        tf.ship = fit.ship
        tf.drones = HandledList(drones)
        tf.fighters = HandledList([])
        tf.boosters = HandledList([])
        tf.extraAttributes = fit.extraAttributes
        tf.mode = fit.mode
        preTraitMultipliers = getCurrentMultipliers(tf)
        for effect in fit.ship.item.effects.values():
            if effect.isImplemented:
                effect.handler(tf, tf.ship, [], None, effect=effect)
        # Factor in mode effects for T3 Destroyers
        if fit.mode is not None:
            for effect in fit.mode.item.effects.values():
                if effect.isImplemented:
                    effect.handler(tf, fit.mode, [], None, effect=effect)
        if fit.ship.item.groupID == getGroup("战略巡洋舰").ID:
            subSystems = list(filter(lambda mod: mod.slot == FittingSlot.SUBSYSTEM and mod.item, fit.modules))
            for sub in subSystems:
                for effect in sub.item.effects.values():
                    if effect.isImplemented:
                        effect.handler(tf, sub, [], None, effect=effect)
        postTraitMultipliers = getCurrentMultipliers(tf)
        getMaxRatio = lambda dictA, dictB, key: max(map(lambda a, b: b / a, dictA[key], dictB[key]))
        multipliers["turret"] = round(getMaxRatio(preTraitMultipliers, postTraitMultipliers, "turrets"), 6)
        multipliers["launcher"] = round(getMaxRatio(preTraitMultipliers, postTraitMultipliers, "launchers"), 6)
        multipliers["droneBandwidth"] = round(getMaxRatio(preTraitMultipliers, postTraitMultipliers, "drones"), 6)
        Fit.getInstance().recalc(fit)
        return multipliers

    @staticmethod
    def getShipSize(groupID):
        # Size groupings are somewhat arbitrary but allow for a more manageable number of top level groupings in a tree structure.
        frigateGroupNames = ["护卫舰", "穿梭机", "Corvette", "Assault Frigate", "Covert Ops", "Interceptor",
                             "隐形轰炸舰", "电子攻击舰", "勘探护卫舰", "后勤护卫舰"]
        destroyerGroupNames = ["驱逐舰", "拦截舰", "战术驱逐舰", "指挥驱逐舰"]
        cruiserGroupNames = ["巡洋舰", "重型突击巡洋舰", "后勤舰", "力场侦察舰",
                             "重型拦截巡洋舰", "战斗侦察舰", "战略巡洋舰"]
        bcGroupNames = ["战斗战列巡洋舰", "指挥舰", "攻击战列巡洋舰"]
        bsGroupNames = ["战列舰", "精英战列舰", "黑隐特勤舰", "掠夺舰"]
        capitalGroupNames = ["泰坦", "无畏舰", "货舰", "航空母舰", "超级航母",
                             "旗舰级工业舰", "战略货舰", "战力辅助舰"]
        indyGroupNames = ["工业舰", "深层空间运输舰", "偷运舰",
                          "采矿驳船", "采掘者", "工业指挥舰"]
        miscGroupNames = ["太空舱", "考察船原型"]
        shipSizes = [
            {"name": "护卫舰", "groupIDs": map(lambda s: getGroup(s).ID, frigateGroupNames)},
            {"name": "驱逐舰", "groupIDs": map(lambda s: getGroup(s).ID, destroyerGroupNames)},
            {"name": "巡洋舰", "groupIDs": map(lambda s: getGroup(s).ID, cruiserGroupNames)},
            {"name": "Battlecruiser", "groupIDs": map(lambda s: getGroup(s).ID, bcGroupNames)},
            {"name": "战列舰", "groupIDs": map(lambda s: getGroup(s).ID, bsGroupNames)},
            {"name": "旗舰", "groupIDs": map(lambda s: getGroup(s).ID, capitalGroupNames)},
            {"name": "工业舰", "groupIDs": map(lambda s: getGroup(s).ID, indyGroupNames)},
            {"name": "Misc", "groupIDs": map(lambda s: getGroup(s).ID, miscGroupNames)}
        ]
        for size in shipSizes:
            if groupID in size["groupIDs"]:
                return size["name"]
        sizeNotFoundMsg = "ShipSize not found for groupID: " + str(groupID)
        return sizeNotFoundMsg

    @staticmethod
    def exportEfs(fit, typeNotFitFlag, callback):
        sFit = Fit.getInstance()
        includeShipTypeData = typeNotFitFlag > 0
        if includeShipTypeData:
            fitName = fit.name
        else:
            fitName = fit.ship.name + ": " + fit.name
        pyfalog.info("Creating Eve Fleet Simulator data for: " + fit.name)
        fitModAttr = fit.ship.getModifiedItemAttr
        propData = EfsPort.getPropData(fit, sFit)
        mwdPropSpeed = fit.maxSpeed
        if includeShipTypeData:
            mwdPropSpeed = EfsPort.getT2MwdSpeed(fit, sFit)
        projections = EfsPort.getOutgoingProjectionData(fit)
        modInfo = EfsPort.getModuleInfo(fit)
        moduleNames = modInfo["moduleNames"]
        modTypeIDs = modInfo["modTypeIDs"]
        weaponSystems = EfsPort.getWeaponSystemData(fit)

        turretSlots = fitModAttr("turretSlotsLeft") if fitModAttr("turretSlotsLeft") is not None else 0
        launcherSlots = fitModAttr("launcherSlotsLeft") if fitModAttr("launcherSlotsLeft") is not None else 0
        droneBandwidth = fitModAttr("droneBandwidth") if fitModAttr("droneBandwidth") is not None else 0
        weaponBonusMultipliers = EfsPort.getWeaponBonusMultipliers(fit)
        effectiveTurretSlots = round(turretSlots * weaponBonusMultipliers["turret"], 2)
        effectiveLauncherSlots = round(launcherSlots * weaponBonusMultipliers["launcher"], 2)
        effectiveDroneBandwidth = round(droneBandwidth * weaponBonusMultipliers["droneBandwidth"], 2)
        # Assume a T2 siege module for dreads
        if fit.ship.item.group.name == "无畏舰":
            effectiveTurretSlots *= 9.4
            effectiveLauncherSlots *= 15
        hullResonance = {
            "exp": fitModAttr("explosiveDamageResonance"), "kin": fitModAttr("kineticDamageResonance"),
            "therm": fitModAttr("thermalDamageResonance"), "em": fitModAttr("emDamageResonance")
        }
        armorResonance = {
            "exp": fitModAttr("armorExplosiveDamageResonance"), "kin": fitModAttr("armorKineticDamageResonance"),
            "therm": fitModAttr("armorThermalDamageResonance"), "em": fitModAttr("armorEmDamageResonance")
        }
        shieldResonance = {
            "exp": fitModAttr("shieldExplosiveDamageResonance"), "kin": fitModAttr("shieldKineticDamageResonance"),
            "therm": fitModAttr("shieldThermalDamageResonance"), "em": fitModAttr("shieldEmDamageResonance")
        }
        resonance = {"hull": hullResonance, "armor": armorResonance, "shield": shieldResonance}
        shipSize = EfsPort.getShipSize(fit.ship.item.groupID)
        # Export at maximum spool for consistency, spoolup data is exported anyway.
        defaultSpoolValue = 1
        spoolOptions = SpoolOptions(SpoolType.SPOOL_SCALE, defaultSpoolValue, True)

        cargoIDs = []
        for cargo in fit.cargo:
            cargoIDs.append(cargo.itemID)

        repairs = EfsPort.getRepairData(fit, sFit)

        def roundNumbers(data, digits):
            if isinstance(data, str):
                return
            if isinstance(data, dict):
                for key in data:
                    if isinstance(data[key], Number):
                        data[key] = round(data[key], digits)
                    else:
                        roundNumbers(data[key], digits)
            if isinstance(data, list) or isinstance(data, tuple):
                for val in data:
                    roundNumbers(val, digits)
            if isinstance(data, Number):
                rounded = round(data, digits)
                if data != rounded:
                    pyfalog.error("Error rounding numbers for EFS export, export may be inconsistent."
                                  "This suggests the format has been broken somewhere.")
            return

        try:
            dataDict = {
                "name": fitName, "ehp": fit.ehp, "droneDPS": fit.getDroneDps().total,
                "droneVolley": fit.getDroneVolley().total, "hp": fit.hp, "maxTargets": fit.maxTargets,
                "maxSpeed": fit.maxSpeed, "weaponVolley": fit.getWeaponVolley(spoolOptions=spoolOptions).total,
                "totalVolley": fit.getTotalVolley(spoolOptions=spoolOptions).total, "maxTargetRange": fit.maxTargetRange,
                "scanStrength": fit.scanStrength, "weaponDPS": fit.getWeaponDps(spoolOptions=spoolOptions).total,
                "alignTime": fit.alignTime, "signatureRadius": fitModAttr("signatureRadius"), "weapons": weaponSystems,
                "scanRes": fitModAttr("scanResolution"), "capUsed": fit.capUsed, "capRecharge": fit.capRecharge,
                "capacitorCapacity": fitModAttr("capacitorCapacity"), "rechargeRate": fitModAttr("rechargeRate"),
                "rigSlots": fitModAttr("rigSlots"), "lowSlots": fitModAttr("lowSlots"),
                "midSlots": fitModAttr("medSlots"), "highSlots": fitModAttr("hiSlots"),
                "turretSlots": fitModAttr("turretSlotsLeft"), "launcherSlots": fitModAttr("launcherSlotsLeft"),
                "powerOutput": fitModAttr("powerOutput"), "cpuOutput": fitModAttr("cpuOutput"),
                "rigSize": fitModAttr("rigSize"), "effectiveTurrets": effectiveTurretSlots,
                "effectiveLaunchers": effectiveLauncherSlots, "effectiveDroneBandwidth": effectiveDroneBandwidth,
                "resonance": resonance, "typeID": fit.shipID, "groupID": fit.ship.item.groupID, "shipSize": shipSize,
                "droneControlRange": fitModAttr("droneControlRange"), "mass": fitModAttr("mass"),
                "unpropedSpeed": propData["unpropedSpeed"], "unpropedSig": propData["unpropedSig"],
                "usingMWD": propData["usingMWD"], "mwdPropSpeed": mwdPropSpeed, "projections": projections,
                "repairs": repairs, "modTypeIDs": modTypeIDs, "moduleNames": moduleNames, "cargoItemIDs": cargoIDs,
                "pyfaVersion": pyfaVersion, "efsExportVersion": EfsPort.version
            }
            # Recursively round any numbers in dicts to 6 decimal places.
            # This prevents meaningless rounding errors from changing the output whenever pyfa changes.
            roundNumbers(dataDict, 6)
        except TypeError as e:
            pyfalog.error("Error parsing fit:" + str(fit))
            pyfalog.error(e)
            dataDict = {"name": fitName + "Fit could not be correctly parsed"}
        export = json.dumps(dataDict, skipkeys=True)

        if callback:
            callback(export)
        else:
            return export
